package classes
{
	import classes.utils.Configuration;
	import classes.utils.Utility;
	
	import flash.desktop.NativeApplication;
	import flash.events.Event;
	import flash.filesystem.File;
	import flash.filesystem.FileMode;
	import flash.filesystem.FileStream;
	import flash.net.FileFilter;
	
	import mx.collections.ArrayCollection;
	import mx.controls.Alert;
	import mx.core.UIComponent;
	import mx.events.CloseEvent;
	import mx.managers.PopUpManager;
	
	import view.AddBudgetCategoryPanel;
	import view.AddTransactionPanel;
	import view.dialog.ApprovalSheduledTransList;
	[Bindable]
	public class TreasuryModel
	{
		
		private static var instance:TreasuryModel;
		
		public var currentUser:User;
		private var _userLoggedIn:Boolean = false;
		public var currentCategories:ArrayCollection = new ArrayCollection();
		public var currentRecipients:ArrayCollection = new ArrayCollection();
		public var activeAccount:Account = null;
		public var mainApplication:Treasury;
		public var addTransaction:AddTransactionPanel;
		public var addBudgetCategory:AddBudgetCategoryPanel;
		public var userKey:String = "";
		public var ranNum:Number = 0;
		
		private var usersFilePath:String = "users.tuf";
		private var appConfigPath:String = "appConfig.tcf";
		private var charset:String = "us-ascii";
		private var approveList:Boolean = false;
		
		
		private var userFileKey:String;
		private var backupKey:String = "f2MUnm0x2kX-??h(03z#u5?#";
		
		private var popWindow:UIComponent = null;
		
		public var defaultCategories:ArrayCollection = new ArrayCollection([
			 new Category("Food"),
			 new Category("Gas"),
			 new Category("Bills"),
			 new Category("Entertainment"),
			 new Category("Paycheck"),
			 new Category("Dividend"),
			 new Category("Interest"),
			 new Category("Other")
		]);
		
		public function TreasuryModel()
		{
			
		}
		
		public static function getInstance():TreasuryModel
		{
			if (instance == null)
			{
				instance = new TreasuryModel();
				
			}
			return instance;
		}
		
		public function get userLoggedIn():Boolean{
			return this._userLoggedIn;
		}
		
		public function set userLoggedIn(b:Boolean):void{
			if(this._userLoggedIn == !b){
				//value is changing
				this._userLoggedIn = b;
				this.mainApplication.setLoggedInStatus(this._userLoggedIn);
			}
			else
				this._userLoggedIn = b;
		}
		
		//TODO: RETURN FALSE IF SOMETHING GOES WRONG
		public function createNewUser(username:String, password:String):Boolean
		{
			this.currentUser = new User();
			this.currentUser.userName = username;
			this.currentUser.password = password;
			this.currentUser.encryptionKey = this.generateEncryptionKey();
			
			this.currentCategories = currentUser.categories.length == 0 ? 
				this.defaultCategories :this.currentUser.categories;
			
			this.currentUser.categories = this.currentCategories;
				
			this.currentRecipients = this.currentUser.recipients;
			
			this.currentUser.dataDirectory = File.documentsDirectory.nativePath + "\\Treasury\\data\\" + this.currentUser.userName + ".tsf";
			
			//since we're using usernames as corresponding file names, check for file existence
			if( (new File(this.currentUser.dataDirectory)).exists){
				Utility.getInstance().showAlert("Cannot create new user:\nUsername '" + username + "' already exists", "Existing Username");
				return false;
			}
			
			//append to users file
			this.writeToUsersFile(this.currentUser.getSimpleUserString(), true);
			
			//write save file
			var encUser:String = this.encodeData(this.currentUser.toWritableString(), this.currentUser.encryptionKey);
			this.writeToFile(encUser, "");
			
			this.userLoggedIn = true;
			
			//load application
			this.mainApplication.loadApp();
			
			return true; 
		}
		
		public function deleteUser(user:User):void
		{
			//TODO: delete user's save file
			//TODO: remove user entry from users file
			//TODO: log out user
		}
		
		public function loginUser(userName:String, password:String):Boolean{
			var users:XMLList = new XMLList(this.loadUsersFile());
			
			for(var i:int = 0; i < users.length(); i++){
				if(userName == users[i].@userName && password == users[i].@password){
					this.userLoggedIn = true;
					this.loadUserData(users[i].@dataDirectory, users[i].@encryptionKey);
					
					for(var j:int = 0; j < this.currentUser.accounts.length; j++){
						var temp:Account = (this.currentUser.accounts.getItemAt(j) as Account);
						if(temp.approvalList.length > 0){
							approveList = true;
						}
					}
					if(approveList){
						PopUpManager.createPopUp(this.mainApplication, ApprovalSheduledTransList, true);
					}
					//generalAlert("Day: " + this.currentUser.birthDay + " Month: " + this.currentUser.birthMonth + " Year: " + this.currentUser.birthYear);	
					return true;
				}
			}
			
			return false;
		}
		private function generalAlert(words2:String):void{
						Utility.getInstance().showAlert(words2, "Attention");		
					
			}
		
		public function logoutUser():Boolean{
			//check for unsaved data??
			//autosave?
			//PopUpManager.createPopUp(this.mainApplication, UserLoginDialog, true);
			
			this.activeAccount = null;
			this.currentUser = null;
			this.userLoggedIn = false;
			
			this.setCurrentUserAutoLogin();	//since currentUser is null, autologin will be removed
			
			this.mainApplication.reinit();
			return true;
		}
		
		public function saveUserData(oldName:String = "", oldFileName:String = ""):void
		{			
			var data:String = this.currentUser.toWritableString();
			data = this.encodeData(data, this.currentUser.encryptionKey);
			
			//write to users file
			if(oldName != ""){
				this.writeToUsersFile(this.currentUser.getSimpleUserString(), false, oldName);
				
				//delete old file
				if(oldFileName != ""){
					var file:File = new File(oldFileName);
					if(file.exists)
						file.deleteFile();
				}
			}
			
			//write user's save file
			this.writeToFile(data, "");
		}
		
		public function backupUserData():void{
			
			var file:File = File.desktopDirectory;
			file = file.resolvePath(this.currentUser.userName + ".tbackup");
			
			file.addEventListener(Event.SELECT, saveBackupFile);
			file.browseForSave("Save Backup File");
		}
		
		public function saveBackupFile(e:Event):void
		{
			var data:String = this.currentUser.toWritableString();
			data = this.encodeData(data, this.backupKey);
			
			var file:File = e.target as File;
			this.writeToFile(data, file.nativePath);
		}
		
		/**
		 * Loads file located at 'path' as the currentUser
		 */
		public function loadUserData(path:String, key:String):void{
			var rawString:String = this.loadSaveFile(path, key);
			
			//TEMP: make sure there are cats/recips
			this.currentUser = new User();
			this.currentUser.parseUserString(rawString);
			
			this.currentCategories = currentUser.categories.length == 0 ? 
				this.defaultCategories :this.currentUser.categories;
				
			this.currentUser.categories = this.currentCategories;
				
			//no default recipients!
			this.currentRecipients = this.currentUser.recipients;
			
			//Utility.getInstance().showAlert("User '" + this.currentUser.userName + "' loaded from " + path, "LOAD");
			
			this.recalculateUserData();
			
			this.mainApplication.loadApp();
		}
		
		//changed function type (private to public) for testing
		//TODO: change function type back
		public function generateEncryptionKey():String{
			var i:int = 0;
			var newKey:String = "";
			while(i < 24){
				ranNum = Math.round((Math.random()*94)+ 32);
				if(ranNum == 60 || ranNum == 62 || ranNum == 34 || ranNum == 39){
					newKey += lookup(ranNum+1);
					i++;
				}
				else
				{
					newKey += lookup(ranNum);
				i++;
				}
			}
			
			
			return newKey;
		}

		//FILE I/O//////////////////////////////////////////
				
		/**
		 * writes 'data' to file at 'path'
		 * if 'path' is empty, writes to currentUser's dataDirectory
		 * returns true if successful, false otherwise
		 * */
		private function writeToFile(data:String, path:String):String{
			//var file:File = new File("C:\\writeTest.txt");	//writes to C:\writeTest.txt
			var file:File;
			
			if(path == ""){
				//write to documentsDirectory/Treasury/data/username.tsf
				
				//writes to <My Documents>\Treasury\data\writeTest.tsf (on Windows)
				file = new File(this.currentUser.dataDirectory);
			}
			else{
				file = new File(path);
			}
			
			var filestream:FileStream = new FileStream();
			filestream.open(file, FileMode.WRITE);
			
			filestream.writeMultiByte(data, this.charset);
			
			filestream.close();
			
			//Utility.getInstance().showAlert("Data written to: " + file.nativePath, "Write success");
			
			return file.nativePath;
		}
		
		private function writeToUsersFile(data:String, append:Boolean, replaceName:String = ""):void{
			var file:File = File.applicationStorageDirectory;
			file = file.resolvePath(this.usersFilePath);
			var filestream:FileStream = new FileStream();
			
			var uFile:XMLList = new XMLList(this.loadUsersFile());
			
			if(!append){
				//find entry to replace and replace with data
				
				for(var i:int = 0; i < uFile.length(); i++){
					if(uFile[i].@userName == replaceName){
						uFile[i] = new XML(data);
						break;
					}
				}
				
			}
			else{
				//append to end of XMLList
				uFile += new XML(data);
			}
			
			var encFile:String = this.encodeData(uFile.toXMLString(), this.userFileKey);
			
			filestream.open(file, FileMode.WRITE);				
			filestream.writeMultiByte(encFile, this.charset);
		}
		
		private function loadSaveFile(path:String, key:String):String{
			var file:File = new File(path);
			
			var raw:String = this.readFile(file);
			return this.decodeData(raw, key);
		}
		
		public function noExistingUsers():Boolean{
			var file:File = File.applicationStorageDirectory;
			file = file.resolvePath(this.usersFilePath);
			
			return !file.exists;
		}
		
		private function loadUsersFile():String{
			var file:File = File.applicationStorageDirectory;
			file = file.resolvePath(this.usersFilePath);
			
			var raw:String = this.readFile(file);
			return this.decodeData(raw, this.userFileKey);
		}
		
		/**
		 *  Loads the applicatoin configuration file.
		 * 	This is where data is kept for auto-login, etc.
		 */
		private function loadApplicationConfigurationFile():void{
			var file:File = File.applicationStorageDirectory;
			file = file.resolvePath(this.appConfigPath);
			
			if(!file.exists){
				//file doesn't exist; create it
				var xml:XML = new XML(
					"<usersFileKey key=\"" + this.generateEncryptionKey() + "\"/>"
				);
				
				var encConfig:String = this.encodeData(xml.toXMLString(), this.backupKey);
				this.writeToFile(encConfig, file.nativePath);
				
				Configuration.loadApplicationConfiguration(xml.toXMLString());
			}
			else{
				var raw:String = this.readFile(file);
				var decConfig:String = this.decodeData(raw, this.backupKey);
				
				Configuration.loadApplicationConfiguration(decConfig);
			}
		}
		
		public function setCurrentUserAutoLogin():void{
			var file:File = File.applicationStorageDirectory;
			file = file.resolvePath(this.appConfigPath);
			
			var raw:String = this.readFile(file);
			
			var configOptions:XMLList = new XMLList(this.decodeData(raw, this.backupKey));
			var exists:Boolean = false;
			
			var userNameToUse:String = this.currentUser == null ? "null" : this.currentUser.userName;
			var passwordToUse:String = this.currentUser == null ? "null" : this.currentUser.password;
			
			var autoLogin:XML = new XML("<autoLogin userName=\"" + userNameToUse + 
			  				"\" password=\"" + passwordToUse + "\"/>");
			
			for(var i:int = 0; i < configOptions.length(); i++){
				var xml:XML = configOptions[i] as XML;
				if( (xml.localName() as String).toUpperCase() == "AUTOLOGIN"){
					configOptions[i] = autoLogin;
					exists = true;
					break;
				}
			}
			
			if(!exists)
				configOptions += autoLogin;
			
			//write config file
			this.writeToFile(this.encodeData(configOptions.toXMLString(), this.backupKey), file.nativePath);
		}
		
		private function readFile(file:File):String{
			var rawString:String = "";
			var filestream:FileStream = new FileStream();
			
			try{
				filestream.open(file, FileMode.READ);
				
				rawString = filestream.readMultiByte(filestream.bytesAvailable, this.charset);
			}
			catch(e:Error){
				//TODO: should we display a message to the user?
			}
			
			filestream.close();
			
			return rawString;
		}
		
		////////////////////////////////////////////////////
		//encodes data from alpha to alpha numeric using the key
		public function encodeData(data:String, key:String):String
		{
			var dataIndex:Number = 0;
			var keyIndex:Number = 0;
			var cryptedText:String = "";
			var keyNum:Number = 0;
			var dataNum:Number = 0;
			var sum:Number = 0;
			
			//backslashes are problematic; use forwards
			data = data.replace( /\\+/g, "/");
			////////////////////
			
			//adds spaces at the end of the string to make it's length a multiple of 3
			if(data.length % 3 == 0){
			}
			else if(data.length % 3 == 1){
				data = data.concat("  ");
			}
			else if(data.length % 3 == 2){
				data = data.concat(" ");
			}
			//rolls through the text until there is no more encrypting it
			while(dataIndex + 2 < data.length){
				
				//gets the value of the slice of data we wish to encrypt
				dataNum = data.charCodeAt(dataIndex + 2) + (data.charCodeAt(dataIndex + 1)*256) + (data.charCodeAt(dataIndex + 0)*65536);	
			    
			    //gets the value of the slice of the key we wish to use to encrypt
				keyNum = key.charCodeAt(keyIndex + 3) + (key.charCodeAt(keyIndex + 2)*256) + (key.charCodeAt(keyIndex + 1)*65536) + (key.charCodeAt(keyIndex + 0)*16777216);
				
				//adds the two values together			
				sum = dataNum + keyNum;
				
				//adds the new number to the end of the crypted text making it alpha-numeric
				cryptedText = cryptedText.concat(sum.toString(36));
				
				//increments the indexes accordingly
				dataIndex = dataIndex+3;
				
				//if we are at the end of the key, start at the beginning
				if(keyIndex == 20){
					keyIndex = 0;
				}
				//otherwise increment 4
				else{
					keyIndex = keyIndex + 4;
				}
				
			}
			
			//return the crypted text
			return cryptedText;
		}
		
		//decodes Data from alpha-numeric to readable alpha text
		public function decodeData(data:String, key:String):String
		{
			
			var dataIndex:Number = 0;
			var keyIndex:Number = 0;
			var textLet:String = "";
			var sum:Number = 0;			
			var keyNum:Number = 0;
			var dataNum:Number = 0;
			var tempNum:uint = 0;
			var output:String = "";
			
		    var tempText:String = "";
			//var tempNum:Number = 0;
			
			while(dataIndex + 5 < data.length){
			
				//get the value of the slice of crypted text
				tempText = data.slice(dataIndex, dataIndex+6);
				sum = value(tempText);
				
					 //generate the key to be used			
					keyNum = key.charCodeAt(keyIndex + 3) + (key.charCodeAt(keyIndex + 2)*256) + (key.charCodeAt(keyIndex + 1)*65536) + (key.charCodeAt(keyIndex + 0)*16777216);
				
					//take out the value of the key so only the data is left		
					dataNum = sum - keyNum;
					
					//create the data from value of the data
					tempNum = dataNum/65536;
					output += lookup(tempNum);
					dataNum = dataNum - (tempNum*65536);
					tempNum = dataNum/256;
					output += lookup(tempNum);
					dataNum = dataNum - (tempNum*256);
					output += lookup(dataNum);
					//increment the indexes
					dataIndex += 6;
					
					if(keyIndex == 20){
						keyIndex = 0;
					}
					else{
						keyIndex = keyIndex + 4;
					}
				//Alert.show("\"" + cyptedtext + "\"");	
				}				
				
				return output;
		}
		
		//private lookup function for decode()
		//turns 'leText' from alphanumeric to decimal
		private function value(leText:String):Number
		{
			var alphaNumeric:String = "0123456789abcdefghijklmnopqrstuvwxyz";
			var indexOfText:Number = 0;
			var indexOfAlpha:Number = 0;
			var numVal:Number = 0;
			var tempString:String = "";
			
			
			
			while(indexOfText < 6)
			{
				tempString = leText.charAt(indexOfText);
				indexOfAlpha = alphaNumeric.indexOf(tempString);
				numVal += (Math.pow(36,5-indexOfText))*indexOfAlpha;
				indexOfText++;
			}
			
			
			return numVal;
		}
	
	//private lookup function for decode()
	//returns the string object of the ascii of value number
	private function lookup(number:uint):String
	{
		if(number > 31){
		var ascii:String= "                                !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~ ";
		ascii = ascii.charAt(number-1);
		}
		else if(number == 10){
			ascii = "\n";
		}
		else if(number == 60 || number == 62 || number == 34){
		    ascii = "error: " + number +  " is out of bounds";
		}
		else
		{
		    ascii = "error: " + number +  " is out of bounds";
		}	
		return ascii;
	}
		
		public function importBackupData():void
		{
			var filter:FileFilter = new FileFilter("Treasury Backup (*.tbackup)", "*.tbackup");
			var bFile:File = new File();
			bFile.addEventListener(Event.SELECT, loadBackupData);
			bFile.browseForOpen("Select Treasury Backup File", [filter]);
		}
		
		private function loadBackupData(e:Event):void{
			var file:File = e.target as File;
			var data:String = "";
			
			if(file.exists){
				data = this.decodeData(this.readFile(file), this.backupKey);
			}
			else{
				//error!
				Utility.getInstance().showAlert("File does not exist!", "Error");
			}
			
			//process the data!!!
			
			var user:User = new User();
			user.parseUserString(data);
			
			//TODO: IMPORT VALIDATION FOR USER INFO!!!!
			
			//if no user is logged in, create a new user, save, and 'logout'
			if(!this.userLoggedIn){
				this.currentUser = user;
				this.saveUserData();
				this.writeToUsersFile(this.currentUser.getSimpleUserString(), true);
				this.logoutUser();
			}
			else{
				if(this.currentUser.userName == user.userName &&
				   this.currentUser.password == user.password)
				{
					this.currentUser = user;
					this.activeAccount = null;
				}
				else{
					//create a new user with the credentials, but keep current user logged in
					var tempUser:User = this.currentUser;
					var tempAcct:Account = this.activeAccount;
					
					this.currentUser = user;
					this.saveUserData();
					this.writeToUsersFile(this.currentUser.getSimpleUserString(), true);
					
					this.currentUser = tempUser;
					this.activeAccount = tempAcct;
					
					Utility.getInstance().showAlert("A New User with the provided information has been created.", "Backup Successful");
				}
			}
		}
		
		public function getCategory(key:String):Category{
			if( key == "")
				return currentCategories.getItemAt(0) as Category;
				
			var i:int = 0;
			for(; i < currentCategories.length; i++){
				if(currentCategories.getItemAt(i).id == key ||
				   currentCategories.getItemAt(i).name.toUpperCase() == key.toUpperCase())
				   return currentCategories.getItemAt(i) as Category;
			}
			
			//TODO: ask to create new one?
			var cat:Category = new Category();
			cat.name = key;
			cat.id = i;
			this.currentUser.categories.addItem(cat);
			
			return cat;
		}
		
		public function getRecipient(name:String):Recipient{
			for(var i:int = 0; i < currentRecipients.length; i++){
				if(currentRecipients.getItemAt(i).name.toUpperCase() == name.toUpperCase())
				   return currentRecipients.getItemAt(i) as Recipient;
			}
			
			//TODO: ask to create new one?
			var r:Recipient = new Recipient();
			r.name = name;
			r.description = "";
			this.currentUser.recipients.addItem(r);
			
			return r;
		}
		
		//Dialog Pop-up controls//////////////////////////
		public function popUp(popup:UIComponent):void{
			this.popWindow = popup;
			PopUpManager.addPopUp(this.popWindow, this.mainApplication, true);
		}
		
		public function removePopUp():void{
			PopUpManager.removePopUp(this.popWindow);
			this.popWindow = null;
		}
		
		public function recalculateUserData():void{
			if(this.userLoggedIn)
				this.currentUser.calculateTotalWorth();
		}
		
		public function deleteActiveAccount(e:CloseEvent = null):void{
			
			if(e == null || e.detail == Alert.YES){
				this.currentUser.deleteAccount(this.activeAccount);
				this.activeAccount = null;
				this.recalculateUserData();
			}
		}
		
		public function saveAndExit(e:CloseEvent):void{
			if(e.detail == Alert.YES && this._userLoggedIn)
				this.saveUserData();
			
			NativeApplication.nativeApplication.exit();
			//TODO: Add a flag for data changes;
			//Use utility function to ask if the
			// user wants to save on exit.
		}
		
		//////////////////////////////////////////////////
		
		public function initData():void{
			this.currentCategories = this.defaultCategories;
			this.loadApplicationConfigurationFile();
		}
		
		public function setUserFileKey(key:String):void{
			this.userFileKey = key;
		}
		
	}
}